package goliath;

import org.jbox2d.collision.AABB;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.joints.*;
import org.jbox2d.util.nonconvex.Polygon;
import pulpcore.Input;
import pulpcore.Stage;
import pulpcore.CoreSystem;
import pulpcore.Assets;
import pulpcore.util.ByteArray;
import pulpcore.platform.applet.AppletAppContext;
import pulpcore.net.Download;
import pulpcore.sprite.Button;
import pulpcore.sprite.Sprite;
import pulpcore.sprite.CycleButton;
import pulpfizz.physics.*;
import pulpfizz.pulp.CameraControlledScene;
import pulpfizz.pulp.body.Java2DBodySprite;
import pulpfizz.pulp.body.PhysicsLayer;
import pulpfizz.pulp.body.BodySprite;
import pulpfizz.pulp.ui.LabelBox;
import pulpfizz.pulp.ui.SmoothMouse;

import java.awt.*;
import java.util.*;
import java.util.List;
import java.applet.Applet;
import java.io.File;

import edit.*;
import draw.JointSprite;

import javax.swing.*;

import level.StackHeightLevel;
import level.AbstractLevel;

/**
 * A simple, over-commented demonstration that shows you how to create a world.
 * When running the example, the arrow keys move the camera, the mouse wheel
 * zooms the camera, and a mouse click will set off a bomb at the mouse location.
 *
 * @author eric
 */
public class BasicDemo extends CameraControlledScene {
    public static int levelNum;
    public static boolean editMode;

    public LabelBox youWinLabel;

    byte[] levelData;

    public BasicDemo(int levelNum, boolean editMode) {
        BasicDemo.levelNum = levelNum;
        BasicDemo.editMode = editMode;
        ByteArray byteArray = Assets.get("Tower" + levelNum + ".dat");
        if(byteArray != null) {
            levelData = byteArray.getData();
        }
        youWinLabel = new LabelBox("You win!",400,300);
    }

    public static Body border;

    enum color {
        red(Color.red),
        blue(Color.blue),
        green(Color.green),
        yellow(Color.yellow),
        white(Color.white);
        Color c;

        Color get() {
            return c;
        }

        color(Color c) {
            this.c = c;
        }
    }

    enum draw {
        Box(new DrawBoxContext()),
        Circle(new DrawCircleContext()),
        Polygon(new DrawPolygonContext()),
        Ellipse(new DrawEllipseContext()),
        RoundBox(new RoundRectangleContext()),
        Pin(new PinContext());
        public EditContext ec;
        draw(EditContext ec) {
            this.ec = ec;
        }
    }

    public static Actor borderActor;

    public Material plastic;
    public EditContext editContext;

    public AbstractLevel level;
    static MouseJoint mj;

    public static boolean isGrabbing() {
        return (mj != null);
    }
    Body mouseBody;
    BodySprite mouseBodySprite;

    Body pickedBody;

    Body prevBody;
    BodySprite prevBodySprite;
    Vec2 physicsPressed;
    Vec2 physicsReleased;

    Vec2 physics2Pressed;
    Vec2 physics2Released;

    ui.ButtonGroup buttonGroup;

    /*
    * CameraControlledScene lets us pan and zoom a PulpFizz scene
    * and do things like follow a particular body.
    *
    * It's usually best to base PulpFizz scenes on this class,
    * though it's not strictly necessary; at the moment, CameraControlledScene
    * is specific to PulpFizz and cannot be used apart from the physics
    * handling.  This may be relaxed in a future version.
    */

    /*
      * Your PhysicsLayer does most of the heavy lifting,
      * making sure that physical objects stay synchronized
      * with their sprites, and actually running the physical
      * simulation.
      */
    private static PhysicsLayer physics;

    public static PhysicsLayer getPhysics() {
        return physics;
    }

    public static NewWorld getWorld() {
        return getPhysics().getWorld();
    }

    public static boolean isFixed() {
        return fixedButton.isSelected();
    }

    public static Color getDrawColor() {
        return color.values()[drawColorButton.getCycleState()].get();
    }

    public static Color getFillColor() {
        return color.values()[fillColorButton.getCycleState()].get();
    }

    public static EditContext getEditContext() {
        return draw.values()[editButton.getCycleState()].ec;
    }

    /*
      * This stuff is not strictly necessary in PulpFizz,
      * but is useful for this example.
      */
    private SmoothMouse sm;
    private LabelBox fpsSprite;
    private LabelBox coords;
    private static CycleButton editButton;
    private Button resetButton;

    private Button downloadButton;

    private Button saveButton;
    private Button loadButton;

    private Button quitButton;

    private static Button fixedButton;

    private static CycleButton drawColorButton;
    private static CycleButton fillColorButton;

    /* Frame rate counter variables. */
    int timeElapsed = 0, numFrames = 0;
    Joint revoluteJoint;
    JointSprite joint;

    @Override
    public void load() {
        super.load();

        Stage.setFrameRate(60);
        setCursor(Input.CURSOR_HAND);

        /*
           * Create a Physics sprite, with objects displaying at 20x their size.
           * This is important because JBox2d uses meters as units, and a meter
           * should almost always never take up just one pixel on screen.
           */
        physics = new PhysicsLayer(20.0);

        /* Set the anchor to the center, and add it to the stage. */
        physics.setAnchor(Sprite.CENTER);
        add(physics);

        /*
           * Size the physics world so it matches the stage.
           * Sometimes you might want to size the world differently,
           * we'll see an example of that in a later demo.
           */
        physics.fitToStage();

        /*
           * Set the gravity to roughly Earth's values.
           * Notice that negative y means "down" in the physics world.
           */
        physics.setGravity(new Vec2(0, -10.0f));


        /* Add a smoothed mouse tracking sprite. */
        sm = new SmoothMouse();
        add(sm);

        buttonGroup = new ui.ButtonGroup();

        fpsSprite = new LabelBox("FPS: 12345", 10, 10);
        addToHUD(fpsSprite);

        coords = new LabelBox("", 10, 30);
//        addToHUD(coords);

        saveButton = Button.createLabeledButton("Save", 350, 0);
        buttonGroup.add(saveButton);

        loadButton = Button.createLabeledButton("Load", 450, 0);
        buttonGroup.add(loadButton);

        fixedButton = Button.createLabeledToggleButton("Fixed", 100, 0);
        buttonGroup.add(fixedButton);

        /* ...and a reset button.  You can also reset with 'r' */
        resetButton = Button.createLabeledButton("Reset", Stage.getWidth() - 10, 10);
        resetButton.setAnchor(Sprite.NORTH_EAST);
        buttonGroup.add(resetButton);

        editButton = CycleButton.createLabeledCycleButton(draw.class, 200, 0);
        buttonGroup.add(editButton);

        int buttonH = editButton.height.getAsInt();
        drawColorButton = CycleButton.createLabeledCycleButton(color.class, 100, buttonH);
        buttonGroup.add(drawColorButton);

        fillColorButton = CycleButton.createLabeledCycleButton(color.class, 200, buttonH);
        buttonGroup.add(fillColorButton);

        quitButton = Button.createLabeledButton("Quit", Stage.getWidth() - 10,resetButton.height.getAsInt()+10);
        quitButton.setAnchor(Sprite.NORTH_EAST);
        addToHUD(quitButton);

        /* The actual physical world is constructed here. */
        createWorld();

        if(editMode) {
            addToHUD(buttonGroup);
        }

        resetLevel();
    }

    public void createWorld() {
        NewWorld world = physics.getWorld();

        /* Add a physical border to the world so our objects can't escape. */
        border = BodyUtils.createBoundingBox(world, 0.5f);

        /* 
        * If you don't do anything else to a newly created body, it will
        * be fixed in space, which is what we want for the border.
        */

        /* 
        * The body for the border is created, now we need to attach a graphic.
        * Java2DBodySprite has a default drawing mode that just renders the
        * geometry.  In another example we'll see how to add your own art, it's
        * very easy to do.
        */
        Java2DBodySprite borderSprite = new Java2DBodySprite(border, physics);
        physics.add(borderSprite);


        /* Some variables to make sure we create bodies within the world bounds. */
        AABB worldAABB = world.getWorldAABB();
        float minX = worldAABB.lowerBound.x + 2.0f;
        float widthX = worldAABB.upperBound.x - minX - 4.0f;
        float minY = worldAABB.lowerBound.y + 2.0f;
        float heightY = worldAABB.upperBound.y - minY - 4.0f;

        /* Create a bunch of circles. */
        for (int i = 0; i < 0; ++i) {
            float myX = minX + (float) (Math.random() * widthX);
            float myY = minY + (float) (Math.random() * heightY);
            float myRad = 0.2f + .5f * (float) Math.random();

            /* Actually create the circle. */
            Body myCircle = BodyUtils.createCircle(world, myX, myY, myRad);

            /*
                * Apply a material to the circle.  This sets properties like
                * mass and friction.
                */

            /*
                * Get in the habit of calling applyAll() when applying
                * material properties - this will apply the material to every
                * shape on the body (a body may have multiple shapes attached
                * to it), whereas apply() only applies Body-specific properties,
                * leaving out things like density, friction and restitution,
                * which "live" on the attached shapes.
                */
            plastic = Material.WOOD();
            plastic.applyAll(myCircle);

            /* Same as what we did to add a graphic to the border... */
            Java2DBodySprite spr = new Java2DBodySprite(myCircle, physics);
            physics.add(spr);

            BodyInfoBox sprite = new BodyInfoBox(myCircle);
            sprite.setLocation(0,50);
//            addToHUD(sprite);
            
        }

        /* Create a bunch of squares.  Mostly the same as above. */
        for (int i = 0; i < 0; ++i) {
            float myX = minX + (float) (Math.random() * widthX);
            float myY = minY + (float) (Math.random() * heightY);
            float mySize = 3f; //+ (float) Math.random();
            float myAngle = (float) (2 * Math.PI * Math.random());
            Body myBox = BodyUtils.createBox(world, myX, myY, mySize, mySize, myAngle);
            plastic.applyAll(myBox);

            /*
                * You can also set the sharpness of the rendering and the
                * coloring of the Java2DBodySprite.
                */
            float sharpness = 3.0f; //much sharper than default 1.0, so zooming in will look clearer
            Color myStroke = Color.RED;
            Color myFill = Color.BLUE;
            Java2DBodySprite spr = new Java2DBodySprite(myBox, physics, sharpness, myStroke, myFill);
            physics.add(spr);
        }

        /* Create a bunch of random polygons. */
        Polygon.B2_POLYGON_REPORT_ERRORS = false; //turn off polygon error reporting, there's no need for it here
        for (int i = 0; i < 0; ++i) {
            float myX = minX + (float) (Math.random() * widthX);
            float myY = minY + (float) (Math.random() * heightY);
            float mySize = 2.0f;
            int nVerts = 3 + (int) (Math.random() * 20);
            Vec2[] myVerts = new Vec2[nVerts];
            for (int j = 0; j < nVerts; ++j) {
                myVerts[j] = new Vec2(myX + mySize * (float) (Math.random() - 0.5),
                        myY + mySize * (float) (Math.random() - 0.5));
            }

            /*
                * createPolygon takes an arbitrary list of vertices,
                * and does its best to create a valid polygon out of
                * it.  It fixes up things like non-counterclockwise
                * ordering, non-convexity, and non-simplicity, all of
                * which you would otherwise have to fix yourself.
                *
                * However, the flipside of this automatic fix-up is
                * that JBox2d's restrictions on shapes cannot always
                * be satisfied, so sometimes the thing just gives up
                * and doesn't create pieces of your polygon.  It will
                * still do its best to create as much valid geometry
                * as possible, but particularly small features are
                * often skipped due to engine restrictions.
                */
            Body myPoly = BodyUtils.createPolygon(world, myVerts);

            /*
                * If you pass weird geometry to createPolygon, the result
                * might come back null if it can't be properly sanitized
                * as geometry.  Here we're making random shapes, so this
                * is a possibility.  So always check for null!
                *
                * You can probably omit the null check if you are creating
                * fairly fat convex polygons (where "fairly fat" means
                * greater than ~0.1 m minimum thickness), but otherwise
                * you should always check, just to be safe, ESPECIALLY
                * if you're accepting user input.
                */
            if (myPoly == null) continue;
            plastic.applyAll(myPoly);

            float sharpness = 3.0f;
            Color myStroke = Color.GREEN;
            Color myFill = Color.GREEN;

            Java2DBodySprite spr = new Java2DBodySprite(myPoly, physics, sharpness, myStroke, myFill);
            physics.add(spr);

            /*
                * The BodyUtils class has useful methods in addition to the body
                * creation routines - here we'll use the cloneBodyWithOffset
                * method to clone the body we've just created and shift its
                * location by (2,0).
                *
                * This method can be very useful for creating chains of objects,
                * but we'll have to wait until we encounter joints (in a later
                * example) to see that.
                */
            Body myPolyClone = BodyUtils.cloneBodyWithOffset(2.0f, 0.0f, myPoly);
            myStroke = Color.RED;
            myFill = Color.RED;
            spr = new Java2DBodySprite(myPolyClone, physics, sharpness, myStroke, myFill);
            physics.add(spr);

        }

        /*
         * Mouse stuff.
         */
//        Body b = BodyUtils.createCircle(
//            world, 6, 0, 0.5f);
//        Material.STYROFOAM().applyAll(b);
//        b.setBullet(true);
//        BodySprite bs = new Java2DBodySprite(b, physics);
//        physics.add(bs);
//        b.allowSleeping(false);
//        MouseJointDef md = new MouseJointDef();
//        md.body1 = world.getGroundBody();
//        md.body2 = b;
//        md.maxForce = 250.0f * PhysicsUtils.getWeight(b);
//        md.dampingRatio = 3f;
//        md.frequencyHz = 50f;
//        mj = (MouseJoint) world.createJoint(md);
//        Sprite s = new JointSprite(mj);
//        add(s);
//
//        mj.m_localAnchor = new Vec2(0, 0);
//        mj.setTarget(b.getPosition());
    }

    List<Vec2> points = new ArrayList<Vec2>();

    Download d;

//Create a file chooser
static JFileChooser fc = new JFileChooser();
//    static {
//        fc.setFileFilter(new XmlFileFilter());
//    }

    boolean won = false;

    @Override
    public void update(int elapsedTime) {
        super.update(elapsedTime);
        updateFPS(elapsedTime);
        level.update(elapsedTime);
        if(level.isLevelWon()) {
            if(!won) {
                physics.add(youWinLabel);
                won = true;
                youWinLabel.setText("You win! Time = "+(startTime/ 1000) + " sec");
            }
        }
        Vec2 mouseVec = new Vec2(
                (float)physics.getPhysicsX(sm.x.get(), sm.y.get()),
                (float)physics.getPhysicsY(sm.y.get(), sm.y.get()));

        if(d != null) {
            System.out.println("download done!");
            byte[] data = d.getData();
            System.out.println("data.length = " + data.length);
            d = null;
        }
        boolean buttonPress = false;
        for(Button b : buttonGroup.buttons) {
            if(b.isMouseDown()) {
                buttonPress = true;
            }
        }
        if(quitButton.isClicked()) {
            Stage.setScene(new TitleScene());
        }
        if(editButton.isClicked()) {
//            if(++currContext > editContexts.size()-1) currContext = 0;
//            editContext.reset();
//            editContext = editContexts.get(currContext);
//            editContext.reset();
//            buttonGroup.remove(editButton);
//            editButton = Button.createLabeledButton(editContext.toString(),200,0);
//            buttonGroup.add(editButton);
//
//            System.out.println("start download");
//             d = Download.startDownload("http://www.google.com/index.html");
//            Stage.pushScene(new DownloadScene(d));
            return;
        } else if(saveButton.isClicked()) {
            // Save the world data to an XML file.
            AppletAppContext context = (AppletAppContext)CoreSystem.getThisAppContext();
            Applet applet = context.applet;
            fc.setApproveButtonText("Save!");
            fc.setDialogTitle("Save World Data");
            if(fc.showSaveDialog(applet) == JFileChooser.APPROVE_OPTION) {
                File outFile = fc.getSelectedFile();
                System.out.println("outFile = "+outFile);
//                if(XmlFileFilter.getExtension(outFile) == null &&
//                        fc.getFileFilter() instanceof XmlFileFilter) {
//                    outFile = new File(outFile.getAbsolutePath() + ".xml");
//                }
                XmlUtil.saveWorld(outFile);
            }
            return;

        } else if(loadButton.isClicked()) {
            // Save the world data to an XML file.
            AppletAppContext context = (AppletAppContext)CoreSystem.getThisAppContext();
            Applet applet = context.applet;
            fc.setApproveButtonText("Load!");
            fc.setDialogTitle("Load World Data");
            if(fc.showOpenDialog(applet) == JFileChooser.APPROVE_OPTION) {
                File inFile = fc.getSelectedFile();
                System.out.println("in = "+inFile);
//                if(XmlFileFilter.getExtension(inFile) == null &&
//                        fc.getFileFilter() instanceof XmlFileFilter) {
//                    inFile = new File(inFile.getAbsolutePath() + ".xml");
//                }
                loadWorld(inFile);
            }
            return;

        } else if (resetButton.isClicked() || Input.isPressed(Input.KEY_R)) {
            //Reset the physics
            physics.resetWorld();

            //Remove all the sprites
            physics.removeAll();

            // The PhysicsLayer is still fit to the original size
            // of the stage, so we don't need to reset it.
            // Also, the camera is not reset.
            createWorld();
            getEditContext().reset();
            resetLevel();
            return;
        } else if (Input.isPressed(Input.KEY_MOUSE_BUTTON_3)) {
            // Delegate right click to active EditContext
//            setCursor(Input.CURSOR_MOVE);
            
            physics2Pressed = new Vec2(
                    (float) physics.getPhysicsX(sm.x.get(), sm.y.get()),
                    (float) physics.getPhysicsY(sm.x.get(), sm.y.get()));

            Body[] at = BodyUtils.getAllBodiesAt(physics.getWorld(), mouseVec);
            Body touched;
            if (at != null) {
//                setCursor(Input.CURSOR_CROSSHAIR);
                System.out.println("bodies=" + at.length);
                if (at.length > 0) {

                    pickedBody = at[0];

//md.maxForce = 10000.0f * body.m_mass;
//<ewjordan>             md.frequencyHz = 20.0f;
//<ewjordan>             md.dampingRatio = 0.9f;

                    touched = at[0];
                    MouseJointDef md = new MouseJointDef();
                    md.body1 = getWorld().getGroundBody();
                    md.body2 = pickedBody;
                    md.maxForce = 2000.0f * md.body2.m_mass;
                    md.dampingRatio = 0.9f;
                    md.frequencyHz = 10.0f;
                    md.collideConnected = false;
                    mj = (MouseJoint) getWorld().createJoint(md);
//                    Sprite s = new JointSprite(mj);
//                    add(s);
                    mj.m_localAnchor = pickedBody.getLocalPoint(mouseVec);
                    mj.setTarget(touched.getPosition());

                }
            }
        } else {
            if(!buttonPress && editMode) {
                EditContext context = draw.values()[editButton.getCycleState()].ec;
                context.update(elapsedTime, mouseVec);
            }
        }

        coords.setText(sm.x.getAsInt()+","+sm.y.getAsInt());

        if(Input.isPressed(Input.KEY_S)) {
//            saveState();
            saveWorld();
        }
        if(Input.isPressed(Input.KEY_L)) {
//            loadState();
            loadWorld();
        }
        
        // Pause/unpause physics updates
        if(Input.isPressed(Input.KEY_SPACE)) {
            physics.active = !physics.active;
        }

        if(Input.isReleased(Input.KEY_MOUSE_BUTTON_3)) {
            setCursor(Input.CURSOR_HAND);
            physics2Released = new Vec2(
                    (float)physics.getPhysicsX(sm.x.get(), sm.y.get()),
                    (float)physics.getPhysicsY(sm.x.get(), sm.y.get()));
            if(mj != null) {
                mj.m_maxForce = 0;
                physics.getWorld().destroyJoint(mj);
                mj = null;
            }
            
            if(revoluteJoint != null) {
                revoluteJoint.destructor();
                remove(joint);
                joint = null;
            }

            pickedBody = null;
        }


        if (pickedBody != null && mj != null) {
            Vec2 vel = pickedBody.getLinearVelocity();
            pickedBody.setAngularVelocity(0.1f * pickedBody.getAngularVelocity());
            pickedBody.setLinearVelocity(new Vec2(0, 0));

            if (pickedBody.m_contactList != null &&
                pickedBody.m_contactList.contact.m_manifoldCount > 0) {
//                System.out.println("Contact!");
            } else {
            }

//                pickedBody.setXForm(mouseVec,pickedBody.getAngle());
//                pickedBody.setBullet(true);
//                pickedBody.applyForce(world.getGravity().mulLocal(0.99f).negate(),mouseVec);
        }


        updateMouse();


        if(firstRun) {
            firstRun = false;
            if(levelData != null) {
                loadWorld(levelData);
            }
        }
    }

    boolean firstRun = true;
    private void updateFPS(int elapsedTime) {
        startTime += elapsedTime;
        fpsSprite.setText("Time=" + (startTime / 1000));
    }

    private void updateMouse() {
        if (physics != null) {
            double pX = physics.getPhysicsX(sm.x.get(), sm.y.get());
            double pY = physics.getPhysicsY(sm.x.get(), sm.y.get());
            if (pX > -physics.getPhysicsWidth() / 2 &&
                    pY > -physics.getPhysicsHeight() / 2) {
                if (mj != null) {
                    mj.setTarget(new Vec2((float) pX, (float) pY));
                }
            }
        }
    }

    public BodySprite setBody(Body b) {
        if (b != null) {
            plastic.applyAll(b);
            Java2DBodySprite myp = new Java2DBodySprite(b, physics, 3, Color.RED, Color.RED);
            physics.add(myp);
            return myp;
        }
        return null;
    }

    static String xString;
    public void saveState() {
        System.out.println("Saving world state!");

        xString = XmlUtil.saveBody(pickedBody);
//        System.out.println("s="+ xString);
   }

    public void loadState() {
        System.out.println("Loading world state!");
        XmlUtil.loadBody(xString,physics.getWorld());
    }

    public void saveWorld() {
        XmlUtil.saveWorld();
    }

    public void loadWorld() {
        // The PhysicsLayer is still fit to the original size
        // of the stage, so we don't need to reset it.
        // Also, the camera is not reset.
        PhysicsLayer p = XmlUtil.loadWorld();
        if(p != null) {
            //Reset the physics
            physics.resetWorld();
            //Remove all the sprites
            physics.removeAll();
            getEditContext().reset();
            this.remove(physics);
            this.add(p);
            physics = p;
            resetLevel();
        }
    }

    public void loadWorld(File inFile) {
        // The PhysicsLayer is still fit to the original size
        // of the stage, so we don't need to reset it.
        // Also, the camera is not reset.
        try {
            PhysicsLayer p = XmlUtil.loadWorld(inFile);
            if(p != null) {
                //Reset the physics
                physics.resetWorld();
                //Remove all the sprites
                physics.removeAll();
                getEditContext().reset();
                this.remove(physics);
                this.add(p);
                physics = p;
                resetLevel();
            }
        } catch (Exception e) {
            System.out.println("Error loading World: "+e.getMessage());
        }
    }

    public void loadWorld(byte[] inByte) {
        try {
            PhysicsLayer p = XmlUtil.loadWorld(inByte);
            if(p != null) {
                //Reset the physics
                physics.resetWorld();
                //Remove all the sprites
                physics.removeAll();
                getEditContext().reset();
                this.remove(physics);
                this.add(p);
                physics = p;
                resetLevel();
            }
        } catch (Exception e) {
            System.out.println("Error loading World: "+e.getMessage());
        }
    }

    public static long startTime;
    private void resetLevel() {
        won = false;
        level = new StackHeightLevel();
        startTime = 0;
    }
}